import EngineUtils from './EngineUtils';
import GLContext from './GLContext';
import { bindAll } from '../utils/utils';
import EventKernel from '../kernel/EventKernel';
import Camera from '../camera/Camera';
import TextureManager from './TextureManager';
import ArraySet from '../kernel/ArraySet';
import AbstractScene from '../scenes/AbstractScene';
import BoxScene from '../scenes/BoxScene';
import Program from './Program';

import { CameraOptions } from '../typings/PanoramaType';

class Engine extends EventKernel {
    private _container: HTMLElement;
    private _rect: DOMRect;
    private _canvas: HTMLCanvasElement;
    private _glContext: GLContext;
    private _camera: Camera;
    private _gl: WebGLRenderingContext;
    private _scenes: ArraySet<AbstractScene> = new ArraySet();
    private _textureMng: TextureManager;

    private _renderTimer?: number;
    private _renderQueue: string[] = [];

    constructor(container: HTMLElement, options: CameraOptions) {
        super();
        bindAll(['_frameRender'], this);
        this._container = container;
        this._rect = this._container.getBoundingClientRect();
        const { width, height } = EngineUtils.getContainerRange(this._rect);
        this._canvas = EngineUtils.createCanvas(width, height, devicePixelRatio);

        this._container.appendChild(this._canvas);
        this._gl = EngineUtils.getWebglRender(this._canvas, {
            antialias: true, // 抗锯齿
            depth: true, // 缓冲区包含深度检测
            premultipliedAlpha: true, // 非预乘阿尔法通道
            preserveDrawingBuffer: false, // 是否保存缓冲区
            alpha: true, // canvas 包含 alpha 通道
            failIfMajorPerformanceCaveat: false, // 低性能设备上下文
        });

        this._gl.viewport(0, 0, width * devicePixelRatio, height * devicePixelRatio);
        this._camera = new Camera(width, height, options);

        // texture mng
        this._textureMng = new TextureManager(this._gl);

        // gl context
        this._glContext = new GLContext(this._gl, {
            width: width * devicePixelRatio,
            height: height * devicePixelRatio,
        });
    }

    getTextureManager() {
        return this._textureMng;
    }

    resize() {
        this._rect = this._container.getBoundingClientRect();
        const { width, height } = this._rect;
        if (width === 0 || height === 0) return;
        this._canvas.width = width * devicePixelRatio;
        this._canvas.height = height * devicePixelRatio;

        this._canvas.style.width = `${width}px`;
        this._canvas.style.height = `${height}px`;

        this._gl.viewport(0, 0, width * devicePixelRatio, height * devicePixelRatio);
        this._glContext.resize(width * devicePixelRatio, height * devicePixelRatio);
        this._camera.resize(width, height);
    }

    render() {
        if (this._renderQueue.length !== 0) return;
        this._renderQueue.push('render');
        this._startRenderQueue();
    }

    private _startRenderQueue() {
        !this._renderTimer && this._frameRender();
    }

    private _frameRender() {
        if (this._renderQueue.length !== 0) {
            this._renderQueue.pop();
            this._draw();
            this._renderTimer = requestAnimationFrame(this._frameRender);
        } else {
            this._stopRenderQueue();
        }
    }

    private _stopRenderQueue() {
        if (this._renderTimer) {
            cancelAnimationFrame(this._renderTimer);
            delete this._renderTimer;
        }
    }

    private _draw() {
        this._glContext.clear();
        for (let i = 0; i < this._scenes.size(); i += 1) {
            const type = this._scenes.get(i).getType();
            if (type === 'Box') {
                this._drawBox(<BoxScene>this._scenes.get(i));
            }
        }
    }

    private _drawBox(scene: BoxScene) {
        this._glContext.use(scene.getShaderName());
        this._glContext.enableAlpha();
        this._gl.blendFuncSeparate(this._gl.SRC_ALPHA, this._gl.ONE_MINUS_SRC_ALPHA, this._gl.ONE, this._gl.ONE_MINUS_SRC_ALPHA);
        this._glContext.disableDepthTest();
        const program = this._glContext.getActiveProgram();
        if (!program) return;
        this._initViewProjectionMatrix(program);

        const a_position = program.getAttribLocation('a_position');
        const a_textCoord = program.getAttribLocation('a_texCoord');
        const u_opacity = program.getUniformLocation('u_opacity');

        const geometryRenderList = scene.getGeometryRenderList();
        for (let i = 0; i < geometryRenderList.length; i += 1) {
            const { buffer, opacity } = geometryRenderList[i];
            buffer.bind(a_position, a_textCoord);
            this._gl.uniform1f(u_opacity, opacity);
            this._gl.drawArrays(this._gl.TRIANGLE_STRIP, 0, buffer.verticesNum);
        }
    }

    private _initViewProjectionMatrix(program: Program) {
        const u_projectionMatrix = program.getUniformLocation('u_projectionMatrix');
        const u_viewMatrix = program.getUniformLocation('u_viewMatrix');
        this._gl.uniformMatrix4fv(u_projectionMatrix, false, this._camera.projectionMatrix);
        this._gl.uniformMatrix4fv(u_viewMatrix, false, this._camera.viewMatrix);
    }

    getGl() {
        return this._gl;
    }

    addScene(scene: AbstractScene) {
        this._scenes.set(scene);
        scene.onAdd(this);
    }

    removeScene(scene: AbstractScene, isDestroy: boolean = true) {
        if (!this._scenes.has(scene)) return;
        this._scenes.delete(scene);
        isDestroy && scene.onRemove();
    }

    getCamera() {
        return this._camera;
    }
}

export default Engine;
